package lelouet.datacenter.simulation;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import lelouet.datacenter.simulation.events.AEvent;
import lelouet.datacenter.simulation.events.TimeComparator;
import lelouet.datacenter.simulation.events.VMStateChanged;
import lelouet.datacenter.simulation.events.VMStateChanged.VMStates;
import lelouet.datacenter.simulation.factorized.WorkingElementImpl;
import lelouet.datacenter.simulation.hypervisors.HypervisorImpl;
import lelouet.datacenter.simulation.vms.AVM;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Physical representation of a datacenter.<br />
 * A datacenter contains hypervisors containing VMs, VMS awaiting to be placed
 * on hypervisors and a manager to handle incoming events and manage the VMs<br />
 * The main use case of a datacenter is
 * <ol>
 * <li>create it, with its {@link HypervisorImpl}s</li>
 * <li>add its manager to the {@link #getEventsListeners()}</li>
 * <li>add startup VMs</li>
 * <li>make the datacenter {@link #work(long)} and let the manager handle
 * incoming events</li>
 * </ol>
 */
public class DataCenter extends WorkingElementImpl {

	private static final Logger mlogger = LoggerFactory
			.getLogger(DataCenter.class);

	/**
	 * @param time
	 *            the last time an action was terminated.
	 */
	public DataCenter(String id, long time) {
		super(id, time);
		mlogger.info("datacenter {} created at time {}", id, time);
	}

	protected HashMap<String, Hypervisor> hypervisors = new HashMap<String, Hypervisor>();

	/** @return the modifiable map of hypervisors, with keys being their names. */
	public Map<String, Hypervisor> getHypervisors() {
		return hypervisors;
	}

	/** the VMs awaiting to be placed on the hypervisors */
	protected LinkedHashSet<VM> awaitingVMs;

	/**
	 * a unmodifiable reference to {@link #awaitingVMs}, cached because it could
	 * be often used
	 */
	protected Set<VM> unmutableAwaitingVMs;
	{
		awaitingVMs = new LinkedHashSet<VM>();
		unmutableAwaitingVMs = Collections.unmodifiableSet(awaitingVMs);
	}

	/**
	 * @return an unmodifiable set of awaiting {@link AVM}s, that is of VMs not
	 *         started on an {@link HypervisorImpl} yet.<br />
	 *         The underlying structure is a {@link LinkedHashSet} so iteration
	 *         are made according to the order of submission
	 */
	public Set<VM> getAwaitingVMs() {
		return unmutableAwaitingVMs;
	}

	/**
	 * Add given {@link AVM} to the awaiting VMs, if there is not one with same
	 * id already running or waiting. This will actually be done on next
	 * {@link #work(long)} with a time of more than 0.
	 * 
	 * @return was there no VM with same id running or waiting yet ?
	 */
	public boolean submitVM(VM vm) {
		for (Hypervisor hp : getHypervisors().values()) {
			if (hp.presentVMs().contains(vm)) {
				return false;
			}
		}
		boolean ret = awaitingVMs.add(vm);
		if (ret) {
			AEvent triggeredEvent = new VMStateChanged(vm, getTime(),
					VMStates.EXTERNAL, VMStates.UNALLOCATED);
			addTriggeredEvent(triggeredEvent);
		}
		return ret;
	}

	/** make the datacenter work until given time is just terminated */
	public List<Event> work(long maxTime) {
		List<Event> ret = new ArrayList<Event>();
		TimeComparator comparator = new TimeComparator();
		while (hasNewTriggeredEvents()) {
			List<Event> lasttriggeredEvents = getTriggeredEvents();
			ret.addAll(lasttriggeredEvents);
			for (Event event : lasttriggeredEvents) {
				for (EventHandler handler : getEventsListeners()) {
					event.apply(handler);
				}
			}
		}
		/** the events generated by the work */
		List<Event> events = new ArrayList<Event>();
		while (getTime() < maxTime) {
			events.clear();
			long nextTime = maxTime;
			for (Hypervisor hp : getHypervisors().values()) {
				long hpNextTime = hp.nextEventTime();
				if (hpNextTime < nextTime) {
					nextTime = hpNextTime;
				}
			}
			for (Hypervisor hp : getHypervisors().values()) {
				events.addAll(hp.work(nextTime));
			}
			Collections.sort(events, comparator);
			ret.addAll(events);
			if (!getEventsListeners().isEmpty()) {
				Collections.sort(events, new TimeComparator());
				for (Event e : events) {
					for (EventHandler eh : getEventsListeners()) {
						ret.addAll(e.apply(eh));
					}
				}
			}
			time = nextTime;
		}
		return ret;
	}

	protected List<EventHandler> eventsListeners = new ArrayList<EventHandler>();

	/**
	 * return the list of {@link EventHandler} this sends messages to.<br />
	 * This list is the one stored, not thread-safe.
	 */
	public List<EventHandler> getEventsListeners() {
		return eventsListeners;
	}

	@Override
	public long nextEventTime() {

		if (hasNewTriggeredEvents()) {
			return getTime();
		}
		long firstEventDate = Long.MAX_VALUE;
		for (Hypervisor hp : hypervisors.values()) {
			long nextTime = hp.nextEventTime();
			if (nextTime < firstEventDate) {
				firstEventDate = nextTime;
			}
		}
		return firstEventDate;
	}

	/** reconfigure every hypervisor */
	@Override
	public void reconfigure() {
		for (Hypervisor hp : hypervisors.values()) {
			hp.reconfigure();
		}
	}

}
